package jeff.plugin.mybatis;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import jeff.plugin.mybatis.code.MyDefaultShellCallback;
import jeff.plugin.mybatis.db.support.Table;
import jeff.plugin.mybatis.util.*;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.h2.tools.RunScript;
import org.h2.tools.Server;
import org.mybatis.generator.api.MyBatisGenerator;
import org.mybatis.generator.api.ProgressCallback;
import org.mybatis.generator.config.Configuration;
import org.mybatis.generator.config.xml.ConfigurationParser;
import org.mybatis.generator.config.xml.MyBatisGeneratorConfigurationParser;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.InputSource;

import jeff.plugin.mybatis.db.DBManager;

/**
 * mybatis代码生成工具
 * @author Jeff
 */
@Mojo(name = "generate", requiresProject = true, requiresDependencyResolution = ResolutionScope.RUNTIME)
public class CodeGenerate extends AbstractMojo {

	@Parameter(defaultValue="false", required=false)
	private boolean debugMode;
	
	@Parameter(defaultValue = "${project}", required=true)
	private MavenProject project;
	
	/** 数据库类型，MYSQL,ORACLE(目前只测试这两个) */
	@Parameter(defaultValue = "MYSQL", required=false)
	private String dbType;
	
	/** 是用老方式生成代码还是新的方式 */
	@Parameter(defaultValue = "false", required=true)
	private boolean isOld;
	/** 采用新模式并考虑使用xml引入table时的模式。 ONLY_TABLE	,ONLY_XML,TABLE_XML; */
	/**
	 * 采用新模式并考虑使用xml引入table时的模式。引入xml必须指定
	 * ONLY_TABLE	只从表中生成代码
	 * ONLY_XML		只从XML中生成代码
	 * TABLE_XML		两者都可以, 同样的table名，xml生成优先
	 */
	@Parameter(defaultValue = "ONLY_TABLE", required=true)
	private String xmlTableMode;
	/** 包含table描述的xml，支持多地址，以逗号分割。当模式为ONLY_XML或者TABLE_XML时, 必须配置该变量 */
	@Parameter(defaultValue = "", required=false)
	private String tableXMLConfigs;

	@Parameter(defaultValue = "/src/main/resources/generator/generatorConfig.xml", required=true)
	private String generateConfig;
	
	/** 通过读取sql创建H2数据库的方式来收集代码 */
	@Parameter(defaultValue = "false", required=false)
	private boolean isGegerateCodeBySql;
	/**sql文件路径, 支持多sql，以逗号分割*/
	@Parameter(required=false)
	private String sqlConfig;
	
	/** 驱动类 */
	@Parameter(defaultValue = "org.h2.Driver", required=true)
	private String jdbcDriverClass;
	/** 连接jdbc的URL */
	@Parameter(defaultValue = "jdbc:h2:tcp://localhost/mem:test", required=true)
	private String jdbcURL;
	/** jdbc连接用户 */
	@Parameter(defaultValue = "sa", required=true)
	private String jdbcUser;
	/** jdbc连接密码 */
	@Parameter(defaultValue = "", required=true)
	private String jdbcPassword;
	
	/** 自定义的plugin */
	@Parameter(defaultValue = "jeff.plugin.mybatis.code.plugin.MyCustomPlugin", required=true)
	private String mapperPlugin;
	
	@Parameter(defaultValue = "target", required=true)
	private String targetJavaProject;
	@Parameter(defaultValue = "com.htdc.entity", required=true)
	private String targetModelPackage;
	@Parameter(defaultValue = "com.htdc.mapper", required=true)
	private String targetMapperPackage;
	@Parameter(defaultValue = "target", required=true)
	private String targetResourcesProject;
	@Parameter(defaultValue = "com.htdc.mapper.xml", required=true)
	private String targetXMLPackage;
	/** table在生成代码时去掉前缀,支持多个,以逗号分割; 该属性仅在从table中生成代码有效 */
	@Parameter(defaultValue = "", required=false)
	private String targetTablePrefixs;
	
	/** when generate entity's code, ignore the trim */
	@Parameter(defaultValue = "false", required=false)
	private boolean trimStrings;
	
	@Override
	public void execute() throws MojoExecutionException, MojoFailureException {
//		String projectPath = project.getBasedir().getAbsolutePath();
		Server server = null;
		try{
			//添加了sql配置文件，则启动h2数据库,用来连接生成代码
			if(isGegerateCodeBySql){
				Assert.notNull(sqlConfig, "请配置sqlConfig");
				
				this.jdbcDriverClass = "org.h2.Driver";
				this.jdbcURL = "jdbc:h2:tcp://localhost/mem:test";
				this.jdbcUser = "sa";
				this.jdbcPassword = "";
				
				String[] sqlConfigs = sqlConfig.split(";");
				getLog().info("-->sqlPath=" + Arrays.toString(sqlConfigs));
				
				server = Server.createTcpServer().start();
				getLog().info("-->启动h2数据库");
				
				Class.forName(this.jdbcDriverClass);
				Connection conn = DriverManager.getConnection(this.jdbcURL, this.jdbcUser, this.jdbcPassword);
				for(String p : sqlConfigs){
					if(p != null && !p.trim().equals("")){
						RunScript.execute(conn, new FileReader(p.trim()));
					}
				}
			    getLog().info("-->创建内存H2, url=" + this.jdbcURL + ", username="+this.jdbcUser + ", password="+this.jdbcPassword);
			}
			
			if(isOld){
				String configPath = generateConfig;
				getLog().info("xml配置路径=" + configPath);
				generateCodeByOld(configPath);
		    }
		    else{
		    	generateCodeByNew();
		    }
			getLog().info("-->收集代码完成");
			
		}
		catch(Exception e){
			getLog().error("", e);
		}
		finally{
			if(server != null){
				server.stop();
			}
		}
		
	}

	private List<Table> getTables(){
		DBManager dd = new DBManager();
		dd.setDriverName(jdbcDriverClass).setUrl(jdbcURL).setUsername(jdbcUser).setPassword(jdbcPassword);
		try {
			dd.open();
			List<Table> tables = dd.getTables();
			return tables;
		} catch (Exception e) {
			e.printStackTrace();
		} finally{
			try {
				dd.close();
			} catch (SQLException e) {
			}
		}
		return null;
	}
	
	/** 按照新方式生成代码 */
	private void generateCodeByNew() throws Exception{
		Assert.isTrue(!isOld, "必须配置isOld=true");
		Assert.isTrue("ONLY_TABLE".equals(xmlTableMode) || "ONLY_XML".equals(xmlTableMode) || "TABLE_XML".equals(xmlTableMode), 
				"请正确配置xmlTableMode, 可配置为 ONLY_TABLE, ONLY_XML, TABLE_XML");
		
		Properties properties = new Properties();
		properties.put("jdbc.driverClass", this.jdbcDriverClass);
		properties.put("jdbc.url", this.jdbcURL);
		properties.put("jdbc.user", this.jdbcUser);
		properties.put("jdbc.password", this.jdbcPassword);
		properties.put("mapper.plugin", this.mapperPlugin);
		properties.put("targetJavaProject", this.targetJavaProject);
		properties.put("targetModelPackage", this.targetModelPackage);
		properties.put("targetMapperPackage", this.targetMapperPackage);
		properties.put("targetResourcesProject", this.targetResourcesProject);
		properties.put("targetXMLPackage", this.targetXMLPackage);
		
		List<Table> tables = getTables();
		
		InputSource inputSource = new InputSource(IOUtils.readInputStreamFromResource("internal/generatorConfig.xml"));
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		factory.setNamespaceAware(true);
		factory.setValidating(true);
		DocumentBuilder builder = factory.newDocumentBuilder();
		Document document = builder.parse(inputSource);
		Element rootNode = document.getDocumentElement();
		
		Element contextElement = DOMUtils.getChildElementByTagName(rootNode, "context");
		
		//check javaModelGenerator
		Element javaModelGeneratorElement = DOMUtils.getChildElementByTagName(contextElement, "javaModelGenerator");
		Assert.notNull(javaModelGeneratorElement, "javaModelGenerator不存在");
		Element jmgeP1 = document.createElement("property");
		jmgeP1.setAttribute("name", "enableSubPackages");
		jmgeP1.setAttribute("value", "true");
		javaModelGeneratorElement.appendChild(jmgeP1);
		Element jmgeP2 = document.createElement("property");
		jmgeP2.setAttribute("name", "trimStrings");
		jmgeP2.setAttribute("value", Boolean.toString(trimStrings));
		javaModelGeneratorElement.appendChild(jmgeP2);
		
		
		//根据数据库类型，来生成一些特殊用法
		if("MYSQL".equals(this.dbType)){
			//指定sql中关键字的特殊符号
			Element beginningDelimiterEelement = document.createElement("property");
			beginningDelimiterEelement.setAttribute("name", "beginningDelimiter");
			beginningDelimiterEelement.setAttribute("value", "`");
			contextElement.appendChild(beginningDelimiterEelement);
			
			Element endingDelimiterEelement = document.createElement("property");
			endingDelimiterEelement.setAttribute("name", "endingDelimiter");
			endingDelimiterEelement.setAttribute("value", "`");
			contextElement.appendChild(endingDelimiterEelement);
		}
		
		if("ONLY_TABLE".equals(xmlTableMode)){
			for(Table t : tables){
				String tableName = t.getName();
				Element newElement = document.createElement("table");
				newElement.setAttribute("tableName", tableName);
				String nname = modifyModelName(t.getName());
				newElement.setAttribute("domainObjectName", converteTableToDomain(nname));
				contextElement.appendChild(newElement);
			}
		}
		else if("TABLE_XML".equals(xmlTableMode)){
			 	Map<String, Element> xmlTableElements = getElementFromTableXML();
				for(Table t : tables){
					String tableName = t.getName();
					Element newElement = xmlTableElements.get(tableName);
					if(newElement == null){
						newElement = document.createElement("table");
						newElement.setAttribute("tableName", tableName);
						String nname = modifyModelName(t.getName());
						newElement.setAttribute("domainObjectName", converteTableToDomain(nname));
					}
					else{
						document.adoptNode(newElement);
					}
					contextElement.appendChild(newElement);
				}
		}
		else if("ONLY_XML".equals(xmlTableMode)){
			Map<String, Element> xmlTableElements = getElementFromTableXML();
			for(Element element : xmlTableElements.values()){
				document.adoptNode(element);
				contextElement.appendChild(element);
			}
		}

		if(debugMode){
			getLog().info("----------xml内容----------");
			getLog().info(XMLUtils.toString(document));
		}
		
		List<String> warnings = new ArrayList<String>();
		MyBatisGeneratorConfigurationParser parser = new MyBatisGeneratorConfigurationParser(properties);
		Configuration config = parser.parseConfiguration(rootNode);
		
		MyDefaultShellCallback shellCallback = new MyDefaultShellCallback();

		MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, shellCallback, warnings);
		myBatisGenerator.generate(new ProgressCallback() {
			@Override
			public void startTask(String taskName) {
				getLog().info("任务开始：	" + taskName);
			}

			@Override
			public void saveStarted(int totalTasks) {
				getLog().info("saveStarted	"+totalTasks);
			}

			@Override
			public void introspectionStarted(int totalTasks) {
				getLog().info("introspectionStarted	"+totalTasks);
			}

			@Override
			public void generationStarted(int totalTasks) {
				getLog().info("generationStarted	"+totalTasks);
			}

			@Override
			public void done() {
				getLog().info("done============================================");
			}

			@Override
			public void checkCancel() throws InterruptedException {
				getLog().info("checkCancel");
			}
		});
	}
	
	private void generateCodeByOld(String xmlConfig) throws Exception {
		List<String> warnings = new ArrayList<String>();
		ConfigurationParser cp = new ConfigurationParser(null, warnings);

		File file = new File(xmlConfig);
		FileInputStream fis = new FileInputStream(file);
		Configuration config = cp.parseConfiguration(fis);

		MyDefaultShellCallback shellCallback = new MyDefaultShellCallback();

		MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config,
				shellCallback, warnings);
		myBatisGenerator.generate(new ProgressCallback() {
			@Override
			public void startTask(String taskName) {
				getLog().info("任务开始：	" + taskName);
			}

			@Override
			public void saveStarted(int totalTasks) {
				getLog().info("saveStarted	"+totalTasks);
			}

			@Override
			public void introspectionStarted(int totalTasks) {
				getLog().info("introspectionStarted	"+totalTasks);
			}

			@Override
			public void generationStarted(int totalTasks) {
				getLog().info("generationStarted	"+totalTasks);
			}

			@Override
			public void done() {
				getLog().info("done============================================");
			}

			@Override
			public void checkCancel() throws InterruptedException {
				getLog().info("checkCancel");
			}
		});
	}
	
	/**
	 * 获取tableXMLConfigs配置文件中所有的table元素
	 * @return
	 * @throws Exception
	 */
	private Map<String, Element> getElementFromTableXML() throws Exception{
		Map<String, Element> map = new HashMap<String, Element>();
		if(StringUtil.isBlank(tableXMLConfigs)){
			return map;
		}
		String[] ts = tableXMLConfigs.split(",");
		for(String s : ts){
			if(StringUtil.isBlank(s)){
				continue;
			}
			InputSource inputSource = new InputSource(new FileInputStream(s.trim()));
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			factory.setNamespaceAware(true);
			factory.setValidating(true);
			DocumentBuilder builder = factory.newDocumentBuilder();
			Document document = builder.parse(inputSource);
			Element rootNode = document.getDocumentElement();
			
			List<Element> elements = DOMUtils.getChildElementsByTagName(rootNode, "table");
			if(elements != null){
				for(Element element : elements){
					String tableName = DOMUtils.getAttribute(element, "tableName");
					if(tableName == null){
						getLog().info(s+"配置文件中一节点为配置tableName属性");
						continue;
					}
                    String tableType = DOMUtils.getAttribute(element, "tableType");
					if("VIEW".equalsIgnoreCase(tableType)){
                        element.setAttribute("enableInsert", "false");
                        element.setAttribute("enableSelectByPrimaryKey", "false");
                        element.setAttribute("enableSelectByExample", "true");
                        element.setAttribute("enableUpdateByPrimaryKey", "false");
                        element.setAttribute("enableDeleteByPrimaryKey", "false");
                        element.setAttribute("enableDeleteByExample", "false");
                        element.setAttribute("enableCountByExample", "true");
                        element.setAttribute("enableUpdateByExample", "false");
                        element.setAttribute("selectByPrimaryKeyQueryId", "false");
                        element.setAttribute("selectByExampleQueryId", "false");
                    }
                    element.removeAttribute("tableType");

					map.put(tableName.toUpperCase(), (Element)element.cloneNode(true));
				}
			}
		}
		return map;
	}
	
	/**
	 * 去掉表的特殊前缀
	 * @param tableName
	 * @return
	 */
	private String modifyModelName(String tableName){
		if(StringUtil.isBlank(targetTablePrefixs)){
			return tableName;
		}
		String[] ts = targetTablePrefixs.split(",");
		for(String t : ts){
			if(tableName.startsWith(t.toUpperCase())){
				return tableName.substring(t.length());
			}
		}
		return tableName;
	}
	
	/**
	 * 表名转化为domain
	 * @param tableName
	 * @return
	 */
	private String converteTableToDomain(String tableName){
		String newtablename = tableName;
		String[] ts = newtablename.split("_");
		StringBuffer sb = new StringBuffer();
		for(String t : ts){
			String tta = t.substring(0, 1).toUpperCase()+t.substring(1).toLowerCase();
			sb.append(tta);
		}
		return sb.toString();
	}

}
